// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_URL_CONVERTER_
#define SCRW_URL_CONVERTER_

#include <sstream>
#include <stdexcept>
#include <string>

#include <libv/cast.h>
#include <libv/check.h>

#include <unicode/regex.h>
#include <unicode/uidna.h>
#include <unicode/schriter.h>
#include <unicode/unistr.h>

#include "common.h"


namespace scrw
{

/** Исключение, генерируемое классом UrlConverter. */
class UrlConverterException : public ::std::exception
{
  public:
    explicit UrlConverterException(const ::std::string& msg) : msg_(msg) {}
    virtual const char* what() const throw() { return msg_.c_str(); }
    virtual ~UrlConverterException() throw() {}

  private:
    ::std::string msg_;
};


/** Непотокобезопасное преобразование URL между Unicode и Ascii-формами.
  * Реализуется в 2 этапа:
  *   - домен конвертируется через punycode (IDN)
  *   - путь — через процентное кодирование. */
class UrlConverter
{
  public:
    UrlConverter()
        : err_(U_ZERO_ERROR),
          // Вообще в протоколе может быть все, что угодно, но кроме латинских
          // символов известные протоколы ничего не поддерживают, зато это
          // ограничение позволит находить больше ошибок.
          //               протокол      домен      порт+путь
          url_splitter_("^([a-zA-Z0-9]++://)([^:/?#]++)(.*+)$", 0, err_)
    {
        VERIFY(U_SUCCESS(err_) == TRUE);
    }



    UrlConverter(const UrlConverter& rhv)
        : err_(U_ZERO_ERROR),
          url_splitter_(
              rhv.url_splitter_.pattern().pattern(),
              rhv.url_splitter_.pattern().flags(),
              err_)
    {
        VERIFY(U_SUCCESS(err_) == TRUE);
    }



    /** Преобразование интернет-адреса из юникода в подмножество ASCII,
      * используемое в URL. Домены кодируются через punycode, пути через
      * процентное кодирование. URL обязательно должен содержать протокол.
      * Метод не потокобезопасен.
      * Адрес должен быть в юникод-форме, то есть не содержать
      * последовательностей "%HH", где H - шестнадцатеричная цифра, в противном
      * случае все встреченные символы '%' будут закодированы через "%25".
      * @exception UrlConverterException если url содержит некорректную
      *            unicode-последовательность,
      *            libv::PrecondException в случае некорректного адреса,
      *            libv::VerifyException в случае неисправимой ошибки. */
    AsciiUrl Unicode2Ascii(const UnicodeUrl& url)
    {
        PRECOND(url.isBogus() == FALSE);

        url_splitter_.reset(url);
        PRECOND(url_splitter_.find() == TRUE);

        // В путь так же попадает номер порта, если он присутствовал в адресе.
        // Но процентное кодирование оставляет все символы в оригинальном виде.
        err_ = U_ZERO_ERROR;
        const icu::UnicodeString protocol = url_splitter_.group(1, err_);
        icu::UnicodeString domain = url_splitter_.group(2, err_);
        const icu::UnicodeString query = url_splitter_.group(3, err_);
        VERIFY(U_SUCCESS(err_) == TRUE);


        // Максимальное увеличение размера символа может быть очень велико,
        // например, для \uFDFA это около 20 символов.
        // Но для русских символов это xn-- + 2 * (число русских букв).
        // Если этого не хватит, uidna_IDNToASCII вернет необходимый размер.
        // Таким образом цикл будет выполняться 1 или 2 раза.
        icu::UnicodeString u_ascii_domain;
        int32_t exp_len = 4 + 2 * domain.length();
        for ( ; ; )
        {
            // "xn--" + punycode
            err_ = U_ZERO_ERROR;
            const int32_t act_len = uidna_IDNToASCII(
                domain.getBuffer(), domain.length(),
                u_ascii_domain.getBuffer(exp_len), exp_len,
                UIDNA_DEFAULT, NULL, &err_);

            if ( U_SUCCESS(err_) == TRUE )
            {
                u_ascii_domain.releaseBuffer(act_len);
                break;
            }
            else if ( err_ == U_BUFFER_OVERFLOW_ERROR )
            {
                u_ascii_domain.releaseBuffer(0);
                exp_len = act_len;
                continue;
            }
            else
            {
                throw UrlConverterException(
                    "Unicode2Ascii conversion failed: bad input string");
            }
        }

        return UnicodeString2String(protocol) +
               UnicodeString2String(u_ascii_domain) +
               UrlPathPercentEncoding(query);
    }



    /** Преобразование интернет-адреса из ASCII в юникод-форму.
      * Домены декодируются через punycode, пути — через процентное кодирование.
      * URL обязательно должен содержать протокол. Если путь содержит '%',
      * считается, что он в ascii-форме — за каждым символом '%' должны
      * следовать 2 HEX-цифры. Если путь не содержит '%' — он посимвольно
      * копируется в результат.
      * @exception UrlConverterException при обнаружении некорректной
      *            последовательности символов
      *            libv::PrecondException в случае некорректного адреса,
      *            libv::VerifyException в случае неисправимой ошибки.
      * @warning метод не потокобезопасен. */
    UnicodeUrl Ascii2Unicode(const UAsciiUrl& url)
    {
        PRECOND(url.isBogus() == FALSE);

        url_splitter_.reset(url);
        PRECOND(url_splitter_.find() == TRUE);

        // В путь так же попадает номер порта, если он присутствовал в адресе.
        // Но процентное кодирование оставляет все символы в оригинальном виде.
        err_ = U_ZERO_ERROR;
        const icu::UnicodeString protocol = url_splitter_.group(1, err_);
        const icu::UnicodeString domain = url_splitter_.group(2, err_);
        const icu::UnicodeString path_query = url_splitter_.group(3, err_);
        VERIFY(U_SUCCESS(err_) == TRUE);

        UnicodeUrl u_domain;
        const int32_t act_len = uidna_IDNToUnicode(
            domain.getBuffer(), domain.length(),
            u_domain.getBuffer(domain.length()), domain.length(),
            UIDNA_DEFAULT, NULL, &err_);

        if ( U_SUCCESS(err_) != TRUE )
        {
            u_domain.releaseBuffer(0);
            throw UrlConverterException(
                "Ascii2Unicode domain conversion failed: bad input string");
        }
        u_domain.releaseBuffer(act_len);


        UnicodeUrl result;
        result += protocol;
        result += u_domain;
        result += UrlAmpHtmlEntityDecoder(
                      UrlPathPercentDecoding(path_query));
        return result;
    }


   /** Преобразование интернет-адреса из ASCII в юникод-форму.
      * Домены декодируются через punycode, пути — через процентное кодирование.
      * URL обязательно должен содержать протокол. Если путь содержит '%',
      * считается, что он в ascii-форме — за каждым символом '%' должны
      * следовать 2 HEX-цифры. Если путь не содержит '%' — он посимвольно
      * копируется в результат.
      * @exception UrlConverterException при обнаружении некорректной
      *            последовательности символов
      *            libv::PrecondException в случае некорректного адреса,
      *            libv::VerifyException в случае неисправимой ошибки.
      * @warning метод не потокобезопасен. */
    UnicodeUrl Ascii2Unicode(const AsciiUrl& url)
    {
        const UAsciiUrl u_ascii_url(url.c_str(), I32(url.size()), US_INV);

        // Предусловие проверит, что u_ascii_url.isBogus() == FALSE;
        return Ascii2Unicode(u_ascii_url);
    }


  private:
    UrlConverter& operator=(const UrlConverter&);

    /** "Процентное кодирование" пути в URL.
      * Метод не потокобезопасен и не генерирует исключения.
      * @sa http://ru.wikipedia.org/wiki/URL -> Кодирование_URL.
      * @sa rfc2396 -> раздел 2. */
    AsciiUrl UrlPathPercentEncoding(const UnicodeUrl& path) const
    {
        if ( path.isEmpty() ) return AsciiUrl("");

        // Кодовая точка unicode представима не более чем 4 байтами в utf-8.
        // Используется именно char, так как это влияет на перегрузку extract.
        ::std::vector<char> utf8_path(path.length() * 4);
        const int32_t l = path.extract(0,path.length(), &utf8_path[0], "utf-8");
        VERIFY(l <= I32(utf8_path.size()));
        utf8_path.resize(l);

        ::std::stringstream r;
        for ( size_t i = 0 ; i != utf8_path.size() ; ++i )
        {
            // char может быть знаковым, а кодовые позиции — положительные.
            const uint8_t c = static_cast<uint8_t>(utf8_path[i]);
            if ( url_reserved(c) || url_unreserved(c) )
                r << (char)c;  // выводит символ
            else
                r << "%" << ::std::hex << (int)c;  // выводит hex-число.
        }

        return r.str();
    }


    // rfc2396 -> раздел 2.
    bool url_reserved(const uint8_t c) const
    {
        return c == ';' || c == '/' || c == '?' || c == ':' || c == '@' ||
               c == '&' || c == '=' || c == '+' || c == '$' || c == ',';
    }


    // rfc2396 -> раздел 2.
    bool url_unreserved(const uint8_t c) const
    {
        return ( 'a' <= c && c <= 'z' ) || ( 'A' <= c && c <= 'Z' ) ||
               ( '0' <= c && c <= '9' ) ||
               c == '-' || c == '_' || c == '.' || c == '!' ||  c == '~' ||
               c == '*' || c == '\''|| c == '(' || c == ')';
    }


    /** Для того, что бы HTML проходил валидацию, все амперсанды в ссылках
      * должны заменяться на HTML-сущность &amp;.
      * Такое использование встречено на http://ej.ru.
      * @sa http://htmlhelp.com/tools/validator/problems.html. */
    UnicodeUrl UrlAmpHtmlEntityDecoder(const UnicodeUrl& url) const
    {
        UnicodeUrl result;
        UChar* res_buf = result.getBuffer(url.length());
        int32_t j = 0;
        for ( int32_t i = 0 ; i < url.length() ; )
        {
            if ( url[i+0] == '&' &&
                 url[i+1] == 'a' &&
                 url[i+2] == 'm' &&
                 url[i+3] == 'p' &&
                 url[i+4] == ';' )
            {
                res_buf[j++] = '&';
                i += 5;
            }
            else
            {
                res_buf[j++] = url[i++];
            }
        }
        result.releaseBuffer(j);
        return result;
    }


    /** "Процентное декодирование" пути в URL.
      * Если путь не содержит '%', он посимвольно копируется в результат,
      * таким образом, декодирование некодированного пути копирует строку.
      * Метод не потокобезопасен.
      * @exception UrlConverterException в случае, если обнаружен символ '%',
      * за которым не следуют 2 hex-цифры.
      * @sa http://ru.wikipedia.org/wiki/URL -> Кодирование_URL.
      * @sa rfc2396 -> раздел 2. */
    UnicodeUrl UrlPathPercentDecoding(const UAsciiUrl& path) const
    {
        if ( path.isEmpty() ) return UnicodeUrl("");

        UnicodeUrl result;
        ::std::vector<char> u8;  // буфер для utf-8 кусков
        icu::StringCharacterIterator it(path);
        for ( UChar32 c = it.first32() ; it.hasNext() ; c = it.next32() )
        {
            if ( c != '%' )
            {
                if ( !u8.empty() )
                {
                    result.append(UnicodeUrl(&u8[0], I32(u8.size()), "utf-8"));
                    u8.clear();
                }

                result.append(c);
            }
            else
            {
                const UChar32 c1 = it.next32();
                const UChar32 c2 = it.next32();
                if ( c2 == icu::StringCharacterIterator::DONE )
                {
                    throw UrlConverterException(
                        "Url percent decoding failed: unexpected EOL");
                }

                const char cc1 = libv::IntegerCast<char>(c1);
                const char cc2 = libv::IntegerCast<char>(c2);
                u8.push_back(Hex2Dec(cc1) * 16 + Hex2Dec(cc2));
            }
        }

        if ( !u8.empty() )
            result.append(UnicodeUrl(&u8[0], I32(u8.size()), "utf-8"));
        return result;
    }


    /** Преобразование одной шестнадцатеричной цифры в десятичное значение.
      * @exception UrlConverterExcetion, если с не hex-цифра. */
    char Hex2Dec(const char c) const
    {
        if ( 'a' <= c && c <= 'f' )
            return c - 'a' + 10;
        else if ( 'A' <= c && c <= 'F' )
            return c - 'A' + 10;
        else if ( '0' <= c && c <= '9' )
            return c - '0';
        else
            throw UrlConverterException(
                "Url percent decoding failed: non-hex symbol found");
    }


    /** Переменная, используемая для получения кода ошибки функций ICU. */
    mutable UErrorCode err_;

    /** Разделение URL по группам на части: протокол, домен, путь.*/
    icu::RegexMatcher url_splitter_;
};

}  // scrw

#endif  // SCRW_MANAGER_
